package com.zhanglinwei.openApiServer.service.impl;

import com.alibaba.fastjson2.JSON;
import com.zhanglinwei.common.SpringContext;
import com.zhanglinwei.common.utils.AssertUT;
import com.zhanglinwei.openApiClient.utils.OpenApiSigner;
import com.zhanglinwei.openApiClient.wrapper.AbstractLambdaWrapper;
import com.zhanglinwei.openApiServer.actuator.DynamicSqlActuator;
import com.zhanglinwei.openApiServer.context.OpenApiContext;
import com.zhanglinwei.openApiServer.context.OpenApiRequestContext;
import com.zhanglinwei.openApiServer.generate.SQLGenerator;
import com.zhanglinwei.openApiServer.properties.OpenApiProperties;
import com.zhanglinwei.openApiServer.service.OpenApiService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.Temporal;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.Optional;

import static com.zhanglinwei.common.constants.StringPoolConstant.*;
import static com.zhanglinwei.openApiClient.constants.OpenApiConstant.*;

@Slf4j
@Service
public class OpenApiServiceImpl implements OpenApiService {

    @Resource
    private DynamicSqlActuator dynamicSqlActuator;
    @Resource
    private OpenApiProperties openApiProperties;

    @Override
    public Object lambdaWrapperQuery(AbstractLambdaWrapper<?, ?> lambdaWrapper) {
        verifyRequest();

        String sql = SCRIPT_OPEN + SQLGenerator.ofLambdaWrapper(lambdaWrapper) + SCRIPT_CLOSE;
        return StringUtils.isBlank(sql) ? null : dynamicSqlActuator.selectList(sql, lambdaWrapper.getParameterMap());
    }

    @Override
    public Object apiCodeQuery(String apiCode, String apiVersion, Map<String, Object> paramMap) {
        verifyRequest();

        String errorInfo = "No Error";
        Object response = null;
        OpenApiContext.OpenApiMetadata openApiMetadata = null;

        try {
            final String errorMsg = String.format("The target method 【%s %s】 does not exist", apiCode, apiVersion);

            openApiMetadata = OpenApiContext.get(apiCode, apiVersion).orElseThrow(() -> new IllegalArgumentException(errorMsg));
            Object targetClass = findTargetClass(openApiMetadata).orElseThrow(() -> new IllegalArgumentException(errorMsg));
            Method targetMethod = findTargetMethod(targetClass, openApiMetadata).orElseThrow(() -> new IllegalArgumentException(errorMsg));

            Object[] methodParam = this.assembleMethodParam(targetMethod, paramMap);
            response = ReflectionUtils.invokeMethod(targetMethod, targetClass, methodParam);
            return response;
        } catch (Throwable ex) {
            errorInfo = ex.getMessage();
            throw ex;
        } finally {
            log.info("## openApi method invoked -- openApiMetadata: {}, request: {}, response: {}, errorInfo: {}", JSON.toJSONString(openApiMetadata), JSON.toJSONString(paramMap), JSON.toJSONString(response), errorInfo);
        }

    }

    private void verifyRequest() {
        if (openApiProperties.getVerifySign()) {
            verifyTimeStamp();
            verifySign();
        }
    }

    private void verifySign() {
        String appKey = OpenApiRequestContext.getOptional(APP_KEY_HEADER_KEY).orElseThrow(() -> new IllegalArgumentException(APP_KEY_HEADER_KEY + " header not found"));
        String sign = OpenApiRequestContext.getOptional(SIGN_HEADER_KEY).orElseThrow(() -> new IllegalArgumentException(SIGN_HEADER_KEY + " header not found"));
        String timestamp = OpenApiRequestContext.getOptional(TIMESTAMP_HEADER_KEY).orElseThrow(() -> new IllegalArgumentException(TIMESTAMP_HEADER_KEY + " header not found"));

        String appSecret = "";
        String signed = OpenApiSigner.sign(appKey, appSecret, timestamp);
        AssertUT.isTrue(signed.equals(sign), "signature error");
    }

    private void verifyTimeStamp() {
        String timestampString = OpenApiRequestContext.getOptional(TIMESTAMP_HEADER_KEY).orElseThrow(() -> new IllegalArgumentException(TIMESTAMP_HEADER_KEY + " header not found"));

        long timestamp = 0L;
        try {
            timestamp = Long.parseLong(timestampString);
        } catch (Exception ex) {
            throw new IllegalArgumentException(TIMESTAMP_HEADER_KEY + " format error");
        }

        AssertUT.isTrue(Math.abs(System.currentTimeMillis() - timestamp) <= openApiProperties.getExpire(), "signature has expired");
    }




    private Optional<Method> findTargetMethod(Object targetClass, OpenApiContext.OpenApiMetadata openApiMetadata) {
        return Arrays.stream(ReflectionUtils.getDeclaredMethods(AopUtils.getTargetClass(targetClass)))
                .filter(method -> method.getName().equals(openApiMetadata.getFullMethodName()))
                .findFirst();
    }

    private Optional<Object> findTargetClass(OpenApiContext.OpenApiMetadata openApiMetadata) {
        String fullClassName = openApiMetadata.getFullClassName();

        Object targetClass = null;
        try {
            targetClass = SpringContext.getBean(Class.forName(fullClassName));
        } catch (ClassNotFoundException ex) {
            targetClass = SpringContext.getBean(fullClassName);
        }
        return Optional.ofNullable(targetClass);
    }

    private Object[] assembleMethodParam(Method targetMethod, Map<String, Object> paramMap) {
        Parameter[] parameters = targetMethod.getParameters();

        if (parameters == null || parameters.length == 0) {
            return null;
        }

        Parameter parameter = parameters[0];
        Class<?> parameterType = parameter.getType();
        if (isNormalType(parameterType)) {
            String paramName = parameter.getName();
            Object paramValue = paramMap.get(paramName);

            return new Object[]{convertValue(paramValue, parameterType)};
        }

        return new Object[]{JSON.parseObject(JSON.toJSONString(paramMap), parameterType)};
    }

    private boolean isNormalType(Class<?> parameterType) {
        if (parameterType.isPrimitive()) {
            return true;
        }
        if (Number.class.isAssignableFrom(parameterType)) {
            return true;
        }
        if (String.class == parameterType) {
            return true;
        }
        if (Boolean.class == parameterType) {
            return true;
        }
        if (Character.class == parameterType) {
            return true;
        }
        if (Date.class == parameterType) {
            return true;
        }
        if (Temporal.class == parameterType) {
            return true;
        }

        return false;
    }

    private Object convertValue(Object value, Class<?> targetType) {
        if (value == null) {
            return defaultValue(targetType);
        }

        if (targetType.isInstance(value)) {
            return value;
        }

        String valueString = value.toString();

        try {
            if (targetType == String.class) {
                return valueString;
            }
            if (targetType == Integer.class || targetType == int.class) {
                return Integer.parseInt(valueString);
            }
            if (targetType == Long.class || targetType == long.class) {
                return Long.parseLong(valueString);
            }
            if (targetType == Double.class || targetType == double.class) {
                return Double.parseDouble(valueString);
            }
            if (targetType == Float.class || targetType == float.class) {
                return Float.parseFloat(valueString);
            }
            if (targetType == Boolean.class || targetType == boolean.class) {
                return parseBoolean(valueString);
            }
            if (targetType == Short.class || targetType == short.class) {
                return Short.parseShort(valueString);
            }
            if (targetType == Byte.class || targetType == byte.class) {
                return Byte.parseByte(valueString);
            }
            if (targetType == Character.class || targetType == char.class) {
                return valueString.charAt(0);
            }

            if (targetType == Date.class) {
                return parseDate(valueString);
            }
            if (targetType == LocalDate.class) {
                return StringUtils.isBlank(valueString) ? null : LocalDate.parse(valueString);
            }
            if (targetType == LocalTime.class) {
                return StringUtils.isBlank(valueString) ? null : LocalTime.parse(valueString);
            }
            if (targetType == LocalDateTime.class) {
                return StringUtils.isBlank(valueString) ? null : LocalDateTime.parse(valueString);
            }


            if (targetType.isEnum()) {
                return Enum.valueOf((Class<? extends Enum>) targetType, valueString);
            }
            if (targetType == BigDecimal.class) {
                return new BigDecimal(valueString);
            }
            if (targetType == BigInteger.class) {
                return new BigInteger(valueString);
            }
        } catch (Exception e) {
            throw new IllegalArgumentException(
                    String.format("The value '%s' cannot be converted to the type %s", value, targetType.getSimpleName()), e);
        }


        try {
            return JSON.parseObject(JSON.toJSONString(value), targetType);
        } catch (Exception e) {
            throw new IllegalArgumentException(
                    String.format("The value '%s' cannot be converted to the type %s", value, targetType.getSimpleName()), e);
        }
    }

    private boolean parseBoolean(String value) {
        if (TRUE.equalsIgnoreCase(value) || ONE.equals(value)) {
            return true;
        }
        if (FALSE.equalsIgnoreCase(value) || ZERO.equals(value)) {
            return false;
        }
        return Boolean.parseBoolean(value);
    }

    private Date parseDate(String value) {
        try {
            if (StringUtils.isNumeric(value)) {
                return new Date(Long.parseLong(value));
            }
            return Date.from(Instant.parse(value));
        } catch (Exception e) {
            try {
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(value);
            } catch (ParseException ex) {
                throw new IllegalArgumentException("Unable to parse the date: " + value);
            }
        }
    }

    private Object defaultValue(Class<?> type) {
        if (type != null && type.isPrimitive()) {
            if (type == boolean.class) return false;
            if (type == char.class) return '\u0000';
            if (type == byte.class) return (byte) 0;
            if (type == short.class) return (short) 0;
            if (type == int.class) return 0;
            if (type == long.class) return 0L;
            if (type == float.class) return 0.0f;
            if (type == double.class) return 0.0;
        }

        return null;
    }

}
